当前位置: Service > Elasticsearch > 选择适合做索引的数据结构

选择适合做索引的数据结构

2021-06-01 分类:Elasticsearch 作者:admin 阅读(26)

假设由我们自己来设计 MySQL 的索引,大概会有哪些选择呢?

①散列表

首先我们应当想到的是散列表,这是一个非常常见且高效的查询、写入的数据结构,对应到 Java 中就是 HashMap。

这个数据结构应该不需要过多介绍了,它的写入效率很高 O(1),比如我们要查询 id=3 的数据时,需要将 3 进行哈希运算,然后再这个数组中找到对应的位置即可。

但如果我们想查询 1≤id≤6 这样的区间数据时,散列表就不能很好的满足了,由于它是无序的,所以得将所有数据遍历一遍才能知道哪些数据属于这个区间。

②有序数组

有序数组的查询效率也很高,当我们要查询 id=4 的数据时,只需要通过二分查找也能高效定位到数据 O(logn)。

同时由于数据也是有序的,所以自然也能支持区间查询;这么看来有序数组适合用做索引咯?

自然是不行,它有另一个重大问题;假设我们插入了 id=2.5 的数据,就得同时将后续的所有数据都移动一位,这个写入效率就会变得非常低。

③平衡二叉树

既然有序数组的写入效率不高,那我们就来看看写入效率高的,很容易就能想到二叉树。

这里我们以平衡二叉树为例:

由于平衡二叉树的特性:左节点小于父节点、右节点大于父节点。

所以假设我们要查询 id=11 的数据,只需要查询 10→12→11 便能最终找到数据,时间复杂度为 O(logn),同理写入数据时也为 O(logn)。

但依然不能很好的支持区间范围查找,假设我们要查询 5≤id≤20 的数据时,需要先查询 10 节点的左子树再查询 10 节点的右子树最终才能查询到所有数据。导致这样的查询效率并不高。

④跳表

跳表可能不像上边提到的散列表、有序数组、二叉树那样日常见的比较多,但其实 Redis 中的 sort set 就采用了跳表实现。这里我们简单介绍下跳表实现的数据结构有何优势。

我们都知道即便是对一个有序链表进行查询效率也不高,由于它不能使用数组下标进行二分查找,所以时间复杂度是 o(n)。

但我们也可以巧妙的优化链表来变相的实现二分查找,如下图:format,png

我们可以为最底层的数据提取出一级索引、二级索引,根据数据量的不同,我们可以提取出 N 级索引。当我们查询时便可以利用这里的索引变相的实现了二分查找。

假设现在要查询 id=13 的数据,只需要遍历 1→7→10→13 四个节点便可以查询到数据,当数越多时,效率提升会更明显。

同时区间查询也是支持,和刚才的查询单个节点类似,只需要查询到起始节点,然后依次往后遍历(链表有序)到目标节点便能将整个范围的数据查询出来。

同时由于我们在索引上不会存储真正的数据,只是存放一个指针,相对于最底层存放数据的链表来说占用的空间便可以忽略不计了。

平衡二叉树的优化

但其实 MySQL 中的 InnoDB 并没有采用跳表,而是使用的一个叫做 B+ 树的数据结构。

这个数据结构不像是二叉树那样大学老师当做基础数据结构经常讲到,由于这类数据结构都是在实际工程中根据需求场景在基础数据结构中演化而来。

比如这里的 B+ 树就可以认为是由平衡二叉树演化而来。刚才我们提到二叉树的区间查询效率不高,针对这一点便可进行优化:

format,png

在原有二叉树的基础上优化后:所有的非叶子都不存放数据,只是作为叶子节点的索引,数据全部都存放在叶子节点。

这样所有叶子节点的数据都是有序存放的,便能很好的支持区间查询。只需要先通过查询到起始节点的位置,然后在叶子节点中依次往后遍历即可。

当数据量巨大时,很明显索引文件是不能存放于内存中,虽然速度很快但消耗的资源也不小;所以 MySQL 会将索引文件直接存放于磁盘中。

这点和后文提到 Elasticsearch 的索引略有不同。由于索引存放于磁盘中,所以我们要尽可能的减少与磁盘的 IO(磁盘 IO 的效率与内存不在一个数量级)。

通过上图可以看出,我们要查询一条数据至少得进行 4 次IO,很明显这个 IO 次数是与树的高度密切相关的,树的高度越低 IO 次数就会越少,同时性能也会越好。

那怎样才能降低树的高度呢?

我们可以尝试把二叉树变为三叉树,这样树的高度就会下降很多,这样查询数据时的 IO 次数自然也会降低,同时查询效率也会提高许多。这其实就是 B+ 树的由来。

使用索引的一些建议

其实通过上图对 B+树的理解,也能优化日常工作的一些小细节;比如为什么需要最好是有序递增的?

假设我们写入的主键数据是无序的,那么有可能后写入数据的 id 小于之前写入的,这样在维护 B+树索引时便有可能需要移动已经写好数据。

如果是按照递增写入数据时则不会有这个考虑,每次只需要依次写入即可。所以我们才会要求数据库主键尽量是趋势递增的,不考虑分表的情况时最合理的就是自增主键。

整体来看思路和跳表类似,只是针对使用场景做了相关的调整(比如数据全部存储于叶子节点)。

ES 索引

MySQL 聊完了,现在来看看 Elasticsearch 是如何来使用索引的。

正排索引

在 ES 中采用的是一种名叫倒排索引的数据结构;在正式讲倒排索引之前先来聊聊和他相反的正排索引。

format,png

以上图为例,我们可以通过 doc_id 查询到具体对象的方式称为使用正排索引,其实也能理解为一种散列表。

本质是通过 key 来查找 value。比如通过 doc_id=4 便能很快查询到 name=jetty wang,age=20 这条数据。

倒排索引

那如果反过来我想查询 name 中包含了 li 的数据有哪些?这样如何高效查询呢?

仅仅通过上文提到的正排索引显然起不到什么作用,只能依次将所有数据遍历后判断名称中是否包含 li ;这样效率十分低下。

但如果我们重新构建一个索引结构:

format,png

当要查询 name 中包含 li 的数据时,只需要通过这个索引结构查询到 Posting List 中所包含的数据,再通过映射的方式查询到最终的数据。

这个索引结构其实就是倒排索引。

Term Dictionary

但如何高效的在这个索引结构中查询到 li 呢,结合我们之前的经验,只要我们将 Term 有序排列,便可以使用二叉树搜索树的数据结构在 o(logn) 下查询到数据。

将一个文本拆分成一个一个独立Term 的过程其实就是我们常说的分词。

而将所有 Term 合并在一起就是一个 Term Dictionary,也可以叫做单词词典。

英文的分词相对简单,只需要通过空格、标点符号将文本分隔便能拆词,中文则相对复杂,但也有许多开源工具做支持(由于不是本文重点,对分词感兴趣的可以自行搜索)。

当我们的文本量巨大时,分词后的 Term 也会很多,这样一个倒排索引的数据结构如果存放于内存那肯定是不够存的,但如果像 MySQL 那样存放于磁盘,效率也没那么高。

Term Index

所以我们可以选择一个折中的方法,既然无法将整个 Term Dictionary 放入内存中,那我们可以为 Term Dictionary 创建一个索引然后放入内存中。

这样便可以高效的查询 Term Dictionary ,最后再通过 Term Dictionary 查询到 Posting List。

相对于 MySQL 中的 B+树来说也会减少了几次磁盘 IO。

format,png

format,png

如果我们是以 j 开头的 Term 进行搜索,首先第一步就是通过在内存中的 Term Index 查询出以 j 打头的 Term 在 Term Dictionary 字典文件中的哪个位置(这个位置可以是一个文件指针,可能是一个区间范围)。

紧接着在将这个位置区间中的所有 Term 取出,由于已经排好序,便可通过二分查找快速定位到具体位置;这样便可查询出 Posting List。

最终通过 Posting List 中的位置信息便可在原始文件中将目标数据检索出来。

更多优化

当然 Elasticsearch 还做了许多针对性的优化,当我们对两个字段进行检索时,就可以利用 Bitmap 进行优化。

比如现在需要查询 name=li and age=18 的数据,这时我们需要通过这两个字段将各自的结果 Posting List 取出。

format,png

最简单的方法是分别遍历两个集合,取出重复的数据,但这个明显效率低下。

这时我们便可使用 Bitmap 的方式进行存储(还节省存储空间),同时利用先天的位与计算便可得出结果。

[1, 3, 5] ⇒ 10101

[1, 2, 4, 5] ⇒ 11011

这样两个二进制数组求与便可得出结果:

10001 ⇒[1, 5]

最终反解出 Posting List 为 [1, 5],这样的效率自然是要高上许多。同样的查询需求在 MySQL 中并没有特殊优化,只是先将数据量小的数据筛选出来之后再筛选第二个字段,效率自然也就没有 ES 高。

当然在最新版的 ES 中也会对 Posting List 进行压缩,具体压缩规则可以查看官方文档,这里就不具体介绍了。

总结

最后我们来总结一下:

format,png

通过以上内容可以看出再复杂的产品最终都是基础数据结构组成,只是会对不同应用场景针对性的优化,所以打好数据结构与算法的基础后再看某个新的技术或中间件时才能快速上手,甚至自己就能知道优化方向。

最后画个饼,后续我会尝试按照 ES 倒排索引的思路做一个单机版的搜索引擎,只有自己写一遍才能加深理解。

————————————————
版权声明:本文为CSDN博主「柯雨恒」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_42628941/article/details/114980994


MySQL索引与Elasticsearch倒排索引原理对比

MySQL索引原理

MySQL主要使用B+树索引结构:

  1. B+树结构

    • 平衡多路搜索树,所有叶子节点位于同一层

    • 非叶子节点只存储键值,不存储数据

    • 叶子节点包含所有键值和数据指针,并通过链表连接

  2. 工作方式

    • 从根节点开始二分查找

    • 通过中间节点导航到叶子节点

    • 在叶子节点进行精确查找或范围扫描

  3. 特点

    • 适合等值查询和范围查询

    • 数据有序存储,便于排序和分组操作

    • 索引和数据通常存储在一起(聚簇索引)

  4. 索引类型

    • 主键索引(聚簇索引)

    • 二级索引(非聚簇索引)

    • 全文索引(特殊实现)

Elasticsearch倒排索引原理

Elasticsearch使用倒排索引(Inverted Index)作为核心数据结构:

  1. 基本结构

    • 词项字典(Term Dictionary):存储所有唯一词项

    • 倒排列表(Posting List):记录每个词项出现的文档ID列表

    • 词项频率(Term Frequency):词项在文档中出现的次数

    • 位置信息(Position):词项在文档中的位置

  2. 工作方式

    • 对文档内容进行分词处理

    • 为每个词项建立到文档的映射

    • 查询时先查找词项,再获取相关文档列表

  3. 优化技术

    • 跳跃表(Skip List):加速倒排列表的遍历

    • FST(Finite State Transducer):压缩存储词项字典

    • Doc Values:列式存储,用于聚合和排序

  4. 特点

    • 适合全文搜索

    • 支持复杂的相关性评分

    • 查询速度快但写入开销较大

主要区别

特性 MySQL索引 Elasticsearch倒排索引
数据结构 B+树 倒排索引+FST
主要用途 精确查找、范围查询 全文搜索、复杂查询
查询类型 适合结构化数据查询 适合非结构化文本查询
写入性能 相对较高 相对较低(需要重建索引)
排序效率 高效(数据有序存储) 需要额外Doc Values支持
存储方式 行式存储 列式存储(Doc Values)
事务支持 完整支持 不支持
分布式能力 有限 原生支持

应用场景选择

  • 选择MySQL索引:需要事务支持、结构化数据、频繁更新、精确查询的场景

  • 选择Elasticsearch倒排索引:需要全文搜索、高亮显示、复杂相关性排序、海量文本处理的场景

两者也可以结合使用,用MySQL处理事务性数据,用Elasticsearch提供搜索功能。

 

「三年博客,如果觉得我的文章对您有用,请帮助本站成长」

赞(0) 打赏

支付宝
微信
0

支付宝
微信
标签:

上一篇:

下一篇:

你可能感兴趣

共有 0 - 选择适合做索引的数据结构

博客简介

精彩评论

  • admin(6年前 (2020-03-09))

    分别用不同厚度的筏板定义,画图后这设置筏板变截面处理。 http://f.fwxgx.co...

    评:新文章!
  • admin(6年前 (2020-03-09))

    分别用不同厚度的筏板定义,画图后这设置筏板变截面处理。 http://f.fwxgx.co...

    评:新文章!
  • admin(6年前 (2020-03-09))

    新增一个框架图! http://biji.jinli.vip/wp-content/upl...

    评:新文章!
  • 一位WordPress评论者(6年前 (2020-02-13))

    嗨,这是一条评论。 要开始审核、编辑及删除评论,请访问仪表盘的“评论”页面。 评论者头像来自...

    评:世界,您好!